package com.example.autumn.io;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Stream;

/**
 * @author liuzhiyong
 * @date 2023/10/16
 * description: 资源解析器, 只负责扫描并列出所有文件, 由客户端决定是找出.class文件, 还是.properties文件
 */
public class ResourceResolver {

    Logger logger = LoggerFactory.getLogger(getClass());

    private final static Map<String, FileSystem> FILE_SYSTEM_CACHE = new HashMap<>();

    /**
     * 扫描的包路径
     */
    String basePackage;

    public ResourceResolver(String basePackage) {
        this.basePackage = basePackage;
    }

    /**
     * 扫描Resorce资源
     * 扫描指定包下的所有文件
     *
     * @param mapper 映射函数, Resource到ClassName的映射,
     *               Resource是文件资源, 中name属性是 文件路径 例如: "org/example/Hello.class". 需要转成类名,例如: "org.example.Hello"
     *               扫描什么样的文件, 映射成什么样的结果,由调用法自己决定, 例如可以扫描以.class结尾的Resource, 映射成类名, 也可以扫描.properties结尾的Resource
     * @return {@link List<R> }
     * @author liuzhiyong
     * @date 2023/10/16
     */
    public <R> List<R> scan(Function<Resource, R> mapper) {
        String basePackagePath = this.basePackage.replace(".", "/");
        String path = basePackagePath;
        try {
            List<R> collector = new ArrayList<>();
            scan0(basePackagePath, path, collector, mapper);
            return collector;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 扫描文件内容
     *
     * @param fileName 文件名称
     * @return {@link List<String> } 扫描文件内容
     * @author liuzhiyong
     * @date 2023/11/10
     */
    public List<String> scanFile(String fileName) {
        List<String> collector = new ArrayList<>();
        try {
            Enumeration<URL> en = getContextClassLoader().getResources(basePackage);
            // 记录读取过的文件夹
//            Set<String> checkJarNameSet = new HashSet<>();
            while (en.hasMoreElements()) {
                URL url = en.nextElement();
                URI uri = url.toURI();
                String uriToString = uriToString(uri);
                // file:/D:/develop/idea-workspace/autumn-framework/step-by-step/create-bean-instances/target/test-classes/com/example/atumn/scan
                String uriStr = removeTrailingSlash(uriToString);
                if (uriStr.startsWith("jar:")) {
                    // 扫描jar包
                    // 在遍历资源时，可能会出现重复读取同一个文件夹的情况。这是因为在某些情况下，同一个文件夹可能会被多个 jar 包所包含，从而导致重复读取。
                    scanFileContent(importJarUriToPath(basePackage, uriToString), fileName, collector);
                } else {
                    // 扫描文件
                    scanFileContent(Paths.get(uri), fileName, collector);
                }
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        return collector;
    }

    /**
     * 扫描文件,将文件构建成资源, 再指定映射函数, 将资源映射成需要的格式
     *
     * @param basePackagePath
     * @param path
     * @param collector
     * @param mapper
     * @return {@link }
     * @author liuzhiyong
     * @date 2023/10/17
     */
    <R> void scan0(String basePackagePath, String path, List<R> collector, Function<Resource, R> mapper) throws IOException, URISyntaxException {
        logger.atDebug().log("scan path: {}", path);
        Enumeration<URL> en = getContextClassLoader().getResources(path);
        // 记录读取过的jar包, 已经读取过的jar包不会重复读取, 在jar包中扫描相同的uri, 使用相同的uri创建文件系统会报错, 根据情况需要添加校验, 或者使用map做个缓存, 相同的url的FileSystem, 从缓存中获取, 不重新创建
        // jar:file:/C:/Users/14141/Desktop/2/tmp-webapp/WEB-INF/lib/autumn-jdbc-1.0.0.jar!/com/example/autumn/
//        Set<String> checkJarNameSet = new HashSet<>();
        while (en.hasMoreElements()) {
            URL url = en.nextElement();
            URI uri = url.toURI();
            // file:/D:/develop/idea-workspace/autumn-framework/step-by-step/create-bean-instances/target/test-classes/com/example/atumn/scan
            String uriToString = uriToString(uri);
            String uriStr = removeTrailingSlash(uriToString);
            // file:/D:/develop/idea-workspace/autumn-framework/step-by-step/create-bean-instances/target/test-classes/
            String uriBaseStr = uriStr.substring(0, uriStr.length() - basePackagePath.length());
            if (uriBaseStr.startsWith("file:")) {
                // 在目录中搜索, 设置扫描的基础uri 去掉file:
                // /D:/develop/idea-workspace/autumn-framework/step-by-step/create-bean-instances/target/test-classes/
                uriBaseStr = uriBaseStr.substring(5);
            }
            if (uriStr.startsWith("jar:")) {
//                if (checkJarNameSet.contains(uriToString)) {
//                    continue;
//                }
                // 扫描jar包
                scanFile(true, uriBaseStr, jarUriToPath(basePackagePath, uri), collector, mapper);
//                checkJarNameSet.add(uriToString);
            } else {
                // 扫描文件
                scanFile(false, uriBaseStr, Paths.get(uri), collector, mapper);
            }
        }
    }


    /**
     * 获取类加载器
     * CLassLoader首先从Thread.currentThread().getContextClassLoader()获取, 获取不到再从当前Class获取
     * 因为Web应用的ClassLoader不是JVM提供的基于Classpath的ClassLoader,
     * 而是Servlet容器提供的CLassLoader, 他不在默认的ClassPath搜索, 而是在/WEB-INF/classes目录和/WEB-INF/lib的所有jar包搜索,
     * 从Thread.currentThread().getContextClassLoader()可以获取到Servlet容器专属的ClassLoader
     *
     * @return {@link ClassLoader }
     * @author liuzhiyong
     * @date 2023/10/16
     */
    ClassLoader getContextClassLoader() {
        ClassLoader cl = null;
        cl = Thread.currentThread().getContextClassLoader();
        if (cl == null) {
            cl = getClass().getClassLoader();
        }
        return cl;
    }

    /**
     * 获取基本包的路径
     *
     * @param basePackagePath 基本包路径
     * @param jarUri          指向jar文件的uri
     * @return {@link Path }
     * @author liuzhiyong
     * @date 2023/10/17
     */
    Path jarUriToPath(String basePackagePath, URI jarUri) throws IOException {
        String key = uriToString(jarUri).split("!")[0];
        FileSystem fileSystem = FILE_SYSTEM_CACHE.get(key);
        if (fileSystem != null) return fileSystem.getPath(basePackagePath);

        // Map.of() 空map
        fileSystem = FileSystems.newFileSystem(jarUri, Map.of());
        FILE_SYSTEM_CACHE.put(key, fileSystem);
        return fileSystem.getPath(basePackagePath);
    }

    Path importJarUriToPath(String path, String jarUri) throws IOException, URISyntaxException {
        String key = jarUri.split("!")[0];
        FileSystem fileSystem = FILE_SYSTEM_CACHE.get(key);
        if (fileSystem != null) return fileSystem.getPath(path);
        URI uri = new URI(key);
        fileSystem = FileSystems.newFileSystem(uri, Map.of());
        FILE_SYSTEM_CACHE.put(key, fileSystem);
        return fileSystem.getPath(path);
    }

    /**
     * 扫描文件构建资源集合
     *
     * @param isJar     是否为jar
     * @param base      基础路径
     * @param root      基础路径对象
     * @param collector 资源集合
     * @param mapper    资源映射函数
     * @author liuzhiyong
     * @date 2023/10/17
     */
    <R> void scanFile(boolean isJar, String base, Path root, List<R> collector, Function<Resource, R> mapper) throws IOException {
        String baseDir = removeTrailingSlash(base);
        // 遍历指定目录下的所有文件, Files::isRegular表示过滤常规文件
        // 在Java中，常规文件是指不是目录或符号链接的普通文件。也就是说，它不是文件夹，也不是指向其他文件或目录的符号链接。
        // 常规文件是文件系统中最常见的文件类型，包括文本文件、二进制文件、图像文件等。它们通常包含实际的数据，而不是指向其他文件或目录的引用。
        // Java编译之后会把内部类编译成单独的文件, 是可以扫描到内部类的
        try (Stream<Path> walk = Files.walk(root)) {
            walk.filter(Files::isRegularFile).forEach(file -> {
                Resource res = null;
                if (isJar) {
                    res = new Resource(baseDir, removeLeadingSlash(file.toString()));
                } else {
                    String path = file.toString();
                    String name = removeLeadingSlash(path.substring(baseDir.length()));
                    res = new Resource("file:" + path, name);
                }
                logger.atDebug().log("found resource: {}", res);
                // 指定映射函数, 将资源映射成对应的类型
                R r = mapper.apply(res);
                if (r != null) {
                    collector.add(r);
                }
            });
        }
    }

    private void scanFileContent(Path root, String fileName, List<String> collector) throws IOException {
        try (Stream<Path> walk = Files.walk(root)) {
            walk.filter(Files::isRegularFile).forEach(path -> {
                if (fileName.equals(path.getFileName().toString())) {
                    try (Stream<String> streams = Files.lines(path)) {
                        streams.forEach(collector::add);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

    /**
     * uri 转 字符串
     *
     * @param uri
     * @return {@link String }
     * @author liuzhiyong
     * @date 2023/10/16
     */
    String uriToString(URI uri) {
        return URLDecoder.decode(uri.toString(), StandardCharsets.UTF_8);
    }

    /**
     * 删除开头的斜杠
     *
     * @param s
     * @return {@link String }
     * @author liuzhiyong
     * @date 2023/10/16
     */
    String removeLeadingSlash(String s) {
        if (s.startsWith("/") || s.startsWith("\\")) {
            s = s.substring(1);
        }
        return s;
    }

    /**
     * 删除尾部的斜杠
     *
     * @param s
     * @return {@link String }
     * @author liuzhiyong
     * @date 2023/10/16
     */
    String removeTrailingSlash(String s) {
        if (s.endsWith("/") || s.endsWith("\\")) {
            s = s.substring(0, s.length() - 1);
        }
        return s;
    }

}
